import asyncio
import re
from bleak import BleakScanner, BleakClient
import psycopg2
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
import pytz

# <-- 配置项 -->
# 设备名（默认）
TARGET_NAME = "LYWSD03MMC"
# 写入 Mac 地址的文件名
MAC_FILE = "Mac.txt"
# 特征码（非必要不要修改）
CHARACTERISTIC_UUID = "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6"
# 默认的数据库信息
DB_CONFIG = {
    "dbname": "thermo-hygrometer",
    "user": "manser",
    "password": "manser",
    "host": "localhost"
}
# 蓝牙连接超时时间（低功耗设备发现时间比较长，设置过短无法连接）
time_out = 600
# <-- 配置项 结束 -->


# 线程池
executor = ThreadPoolExecutor(max_workers=3)

# MAC 地址校验
def validate_mac(mac):
    pattern = r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
    return re.match(pattern, mac) is not None

# 获取 MAC 地址
async def get_mac_address():
    try:
        with open(MAC_FILE, "r") as f:
            saved_mac = f.read().strip()
            if validate_mac(saved_mac):
                print(f"使用 Mac 地址 〔{saved_mac}〕")
                return saved_mac
            print(f"〔{MAC_FILE}〕 中的 Mac 地址格式错误，重新扫描设备…")
    except FileNotFoundError:
        print(f"找不到 〔{MAC_FILE}〕，正在扫描设备...")

    device = await BleakScanner.find_device_by_filter(
        lambda d, ad: d.name == TARGET_NAME,
        timeout=time_out
    )
    if not device:
        raise RuntimeError(f"找不到设备 〔{TARGET_NAME}〕")

    with open(MAC_FILE, "w") as f:
        f.write(device.address)
    print(f"成功找到设备 {device.name}，已将 Mac 地址 〔{device.address}〕保存到 {MAC_FILE}")
    return device.address

# 数据解析
def parse_data(data: bytes):
    if len(data) != 5:
        raise ValueError(f"数据长度错误: {len(data)}")
    return {
        "temperature": (data[1] << 8 | data[0]),  # 单位：摄氏度
        "humidity": data[2],                      # 单位：%
        "voltage": (data[4] << 8 | data[3])       # 单位：毫伏（mV）
    }

# 数据写入数据库
def insert_to_db(data_dict: dict):
    try:
        conn = psycopg2.connect(**DB_CONFIG)
        cur = conn.cursor()
        cur.execute("""
            INSERT INTO thermo_hygrometer 
            (temperature, humidity, voltage) 
            VALUES (%s, %s, %s)
        """, (data_dict["temperature"], data_dict["humidity"], data_dict["voltage"]))
        conn.commit()
    except Exception as e:
        print(f"数据库错误: {e}")
    finally:
        if 'cur' in locals():
            cur.close()
        if 'conn' in locals():
            conn.close()

# 蓝牙通知处理
async def notification_handler(_, data: bytearray):
    print(f"\n原始数据: {data.hex().upper()}")
    try:
        parsed = parse_data(data)
        now = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y/%m/%d %H:%M:%S')
        print(
            f"* {now} | 温度 〔{(parsed['temperature']/100):.2f}℃〕 | "
            f"湿度 〔{parsed['humidity']}%〕 | 电压 〔{parsed['voltage']}mV〕"
        )
        await asyncio.get_event_loop().run_in_executor(executor, insert_to_db, parsed)
    except Exception as e:
        print(f"数据处理错误: {e}")

# 主函数
async def main():
    try:
        mac_address = await get_mac_address()
    except Exception as e:
        print(f"无法获取 MAC 地址：{e}")
        return

    retry_count = 0
    # 第一次错误冷却 30s，其他同理
    backoff_times = [30, 60, 300]

    while True:
        try:
            async with BleakClient(mac_address, timeout=time_out) as client:
                if not client.is_connected:
                    raise Exception("连接失败")

                print("连接状态 〔· 成功〕")
                await client.start_notify(CHARACTERISTIC_UUID, notification_handler)
                print("正在接收数据，按 Ctrl+C 停止")

                while True:
                    await asyncio.sleep(1)

        except KeyboardInterrupt:
            exit("\n手动退出程序")
        except Exception as e:
            print(f"连接断开或发生错误：{e}")

            if retry_count >= len(backoff_times):
                print("超过最大重试次数，退出程序")
                break

            print(f"第 {retry_count + 1} 次重试，冷却时间 {backoff_times[retry_count]} 秒...")
            await asyncio.sleep(backoff_times[retry_count])
            retry_count += 1
            continue

        # 如果正常退出（如成功重新连接），重置重试计数
        retry_count = 0

    # 程序退出前关闭线程池
    executor.shutdown(wait=False)

# 主程序
if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        exit("\n手动退出程序")
    except Exception as e:
        print(f"运行错误：{e}")
    finally:
        executor.shutdown(wait=False)
